package fi.bluepill.server.rest;

import com.mongodb.*;
import fi.bluepill.server.model.Location;
import fi.bluepill.server.model.LocationBinary;
import fi.bluepill.server.model.LongLat;
import fi.bluepill.server.model.UserAccount;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;
import rip.core.Transactional;

import java.nio.charset.Charset;

public class LocationService extends BluepillService {

    @Transactional
    public void create() {
        assertAuthentication();
        LocationCreateMessage input = read(LocationCreateMessage.class);

        DBCollection col = findCollection(input.getLatitude(), input.getLongitude());

        if (col == null) {
            error("Could not find a collection for these coordinates.");
            return;
        }

        DBObject existing = mongoSearch(col, input.getLatitude(), input.getLongitude(), 0.1);

        if (((BasicDBList)existing.get("locations")).size() > 0) {
            error("Existing location too near.");
            return;
        }

        UserAccount user = user(userId());

        Location location = new Location();
        location.setLongLat(new LongLat(input.getLongitude(), input.getLatitude()));
        location.setOwnerId(user.getUuid());
        location.setOwnerUsername(user.getUsername());

        jpa().persist(location);
        jpa().merge(user);
        jpa().flush();

        BasicDBObject poi = new BasicDBObject();

        BasicDBList loc = new BasicDBList();
        loc.add(input.getLatitude());
        loc.add(input.getLongitude());

        poi.put("loc", loc);
        poi.put("creatorUserId", user.getUuid());
        poi.put("ownerUserId", user.getUuid());
        poi.put("locationId", location.getUuid());
        poi.put("name", input.getName());
        poi.put("amenity", input.getAmenity());

        col.insert(poi);

        BasicDBObject index = new BasicDBObject();
        index.put("loc", "2d");
        col.ensureIndex(index);

        ObjectNode m = okMessage();
        m.put("uuid", location.getUuid());

        write(m);
    }

    private DBCollection findCollection(double lat, double lon) {
        DBCollection col = null;

        for (String collectionName : mongo().getCollectionNames()) {
            col = mongo().getCollection(collectionName);

            DBCursor cursor = col.find(new BasicDBObject("id", "bbox"));

            if (cursor.hasNext()) {
                DBObject bbox = cursor.next();

                double latMin = (Double)bbox.get("latMin");
                double latMax = (Double)bbox.get("latMax");
                double lonMin = (Double)bbox.get("lonMin");
                double lonMax = (Double)bbox.get("lonMax");

                if (lat > latMin && lat < latMax && lon > lonMin && lon < lonMax)
                    break;
            }

            col = null;
        }

        return col;
    }

    public void find() {
        LocationFindMessage input = read(LocationFindMessage.class);

        DBCollection col = findCollection(input.getLatitude(), input.getLongitude());

        if (col == null) {
            error("Could not find a collection for these coordinates.");
            return;
        }

        DBObject result = mongoSearch(col, input.getLatitude(), input.getLongitude(), input.getRadius());

        write(result);
    }

    private DBObject mongoSearch(DBCollection col, Double latitude, Double longitude, Double radius) {
        BasicDBObject query = new BasicDBObject();
        BasicDBObject loc = new BasicDBObject();
        BasicDBList near = new BasicDBList();

        near.add(0, latitude);
        near.add(1, longitude);

        loc.put("$near", near);

        if (radius != null)
            loc.put("$maxDistance", radius);

        query.put("loc", loc);

        DBCursor curs = col.find(query);

        DBObject result = new BasicDBObject();
        result.put("status", "ok");

        BasicDBList list = new BasicDBList();

        while (curs.hasNext()) {
            DBObject row = curs.next();
            DBObject stripped = new BasicDBObject();

            stripped.put("lat", ((BasicDBList)row.get("loc")).get(0));
            stripped.put("lon", ((BasicDBList)row.get("loc")).get(1));

            stripped.put("amenity", row.get("amenity"));

            if (row.containsField("name"))
                stripped.put("name", row.get("name"));

            list.add(stripped);
        }

        result.put("locations", list);

        return result;
    }

    public void get() {
        LocationGetMessage input = read(LocationGetMessage.class);

        Location location = jpaQuery(Location.class, "select l from Location l where l.uuid = :uuid", qp("uuid", input.getLocationId()));

        ObjectNode m = okMessage();

        m.put("entityId", location.getUuid());
        m.put("ownerId", location.getOwnerId());
        m.put("ownerUsername", location.getOwnerUsername());
        m.put("long", location.getLongLat().getLongtitude());
        m.put("lat", location.getLongLat().getLatitude());

        write(m);
    }

    @Transactional
    public void addBinary() {
        assertAuthentication();

        UserAccount user = user(userId());

        LocationAddBinaryMessage input = read(LocationAddBinaryMessage.class);

        if (input.getLocationId() == null) {
            LocationCreateMessage createMessage = new LocationCreateMessage();
            createMessage.setAmenity("user-content");
            createMessage.setLatitude(input.getLatitude());
            createMessage.setLongitude(input.getLongitude());
            createMessage.setName("Content by " + user.getUsername());
            call("create", createMessage);
        }

        Location location = jpaQuery(Location.class, "select l from Location l where l.uuid = :uuid", qp("uuid", input.getLocationId()));

        if (jpaExists(
                "select lb from LocationBinary lb where lb.ownerId = :ownerId and lb.locationId = :locationId",
                qp("ownerId", user.getUuid()),
                qp("locationId", location.getUuid())))
        {
            error("This user already has a stored item on this location.");
            return;
        }

        LocationBinary binary = new LocationBinary();

        binary.setMimeType(input.getMimeType());
        binary.setOwnerId(user.getUuid());
        binary.setOwnerUsername(user.getUsername());
        binary.setData(base64Decode(input.getDataBase64()).getBytes(Charset.forName("UTF-8")));

        jpa().persist(binary);

        location.addBinary(binary);

        jpa().merge(location);

        write(okMessage());
    }

    public void binaries() {
        assertAuthentication();

        LocationGetMessage input = read(LocationGetMessage.class);

        Location location = jpaQuery(Location.class, "select l from Location l where l.uuid = :uuid", qp("uuid", input.getLocationId()));

        if (location == null) {
            error("No location found.");
            return;
        }

        ObjectNode message = okMessage();

        ArrayNode binaries = json().createArrayNode();

        for (LocationBinary binary : location.getBinaries()) {
            ObjectNode binaryNode = json().createObjectNode();
            binaryNode.put("username", binary.getOwnerUsername());
            binaryNode.put("data", base64Encode(binary.getData()));
            binaryNode.put("mimeType", binary.getMimeType());

            binaries.add(binaryNode);
        }

        message.put("binaries", binaries);

        write(message);
    }
}
